﻿/*
@id {7eeff186-cfb4-f7c3-21f2-a15f210dca49}
@name FakeSmile
@version 0.3.0
@description SMIL implementation in ECMAScript
@creator David Leunen (leunen.d@gmail.com)
@homepageURL http://leunen.me/fakesmile/
@ff_min_version 2.0
@ff_max_version 3.*
*/
// ==UserScript==
// @name smil
// @namespace svg.smil
// ==/UserScript==

/* MIT or GPLv3 Licenses */

/*
Copyright 2008 David Leunen
Copyright 2012 Helder Magalhaes
*/

/**
 * Milliseconds Per Frame - relation between animation smoothness and resources usage:
 * 83 for ~12fps (standard quality web animation; low CPU usage; slightly jumpy; recommended for discrete or slow-motion animations);
 * 67 for ~15fps (high quality web animation; reasonable resources usage; recommended for most use-cases);
 * 40 for  25fps ("cine"-look; recommended for good quality animations on television systems);
 * 33 for ~30fps (half LCD refresh rate; recommended for high quality animations on desktop systems);
 * 25 for  40fps (very smooth animation; recommended for high quality animations on dedicated desktop systems);
 * 17 for ~60fps (LCD refresh rate; high CPU and system overhead; only recommended for very high quality animations running on high-end systems).
 * Lower values are *not* recommended - may cause an overall negative impact on the Operating System and a relevant energy consumption increase!
 * References:
 * http://animation.about.com/od/faqs/f/faq_fpsnumber.htm
 * http://en.wikipedia.org/wiki/Frame_rate#Frame_rates_in_film_and_television
 * https://www.nczonline.net/blog/2011/12/14/timer-resolution-in-browsers/
 */
var mpf = 67;
var splinePrecision = 25;

var svgns = "http://www.w3.org/2000/svg";
var smilanimns = "http://www.w3.org/2001/smil-animation";
var smil2ns = "http://www.w3.org/2001/SMIL20";
var smil21ns = "http://www.w3.org/2005/SMIL21";
var smil3ns = "http://www.w3.org/ns/SMIL30";
var timesheetns = "http://www.w3.org/2007/07/SMIL30/Timesheets";
var xlinkns = "http://www.w3.org/1999/xlink";

var animators = new Array(); // all animators
var id2anim = new Object(); // id -> animation elements (workaround a Gecko bug)
var animations = new Array(); // running animators
var timeZero; // timeline start-up timestamp
var prevTime; // previous render timestamp
var animTimer; // render loop timer id, when active

/**
 * If declarative animations are not supported,
 * the document animations are fetched and registered.
 */
function initSMIL() {
    if (document.documentElement.getAttribute("smiling") == "fake")
        return;
    document.documentElement.setAttribute("smiling", "fake");
    smile(document);

    timeZero = new Date();
    prevTime = new Date(0); // not yet rendered

    // I schedule them (after having instantiating them, for sync-based events)
    // (it doesn't work either: first 0s animation don't trigger begin event to the following -> make it asynchronous)
    for (var i = 0, j = animators.length; i < j; ++i)
        animators[i].register();
}

function getURLCallback(data) {
    if (data.success)
        smile(parseXML(data.content, document));
}

function xhrCallback() {
    if (this.readyState == 4 && this.status == 200 && this.responseXML != null)
        smile(this.responseXML);
}

function smile(animating) {
    var request = null;
    var src = null;

    var impl = document.implementation;
    // namespace-to-process cache
    // ("process" in the sense of "feature check states that support by script is needed")
    // (map is initialized this way to avoid variables names being picked up as key instead of their value)
    var ns2proc = {};
    // NOTE: feature strings are broken in ASV - apparently only declarative switch declarations work
    // (we have already filter this implementation, though, during the loading phase)
    // http://tech.groups.yahoo.com/group/svg-developers/message/61236
    ns2proc[svgns] = !impl.hasFeature("http://www.w3.org/TR/SVG11/feature#Animation", "1.1"); //&& !impl.hasFeature("org.w3c.svg.animation", "1.0");
    ns2proc[smilanimns] = !impl.hasFeature(smilanimns, "1.1");
    ns2proc[smil2ns] = !impl.hasFeature(smil2ns, "2.0");
    ns2proc[smil21ns] = !impl.hasFeature(smil21ns, "2.1");
    ns2proc[smil3ns] = !impl.hasFeature(smil3ns, "3.0");
    ns2proc[timesheetns] = !impl.hasFeature(timesheetns, "1.0");

    var animates = animating.getElementsByTagName("*");
    for (var i = 0, j = animates.length; i < j; ++i) {
        var anim = animates.item(i);
        var nodeName = anim.localName;
        var namespaceURI = anim.namespaceURI;

        switch (nodeName.length) {
            case 4: // "link".length
                if ((nodeName == "link" || nodeName == "LINK") && anim.getAttribute("rel") == "timesheet" && anim.getAttribute("type") == "application/smil+xml") {
                    src = anim.getAttribute("src");
                    if (src)
                        break;
                }
                continue;
            case 9: // "timesheet".length
                if (nodeName == "timesheet" && ns2proc[anim.namespaceURI]) {
                    src = anim.getAttribute("href");
                    if (src)
                        break;
                }
                continue;
            case 3: // "set".length
                if (nodeName == "set") {
                    break;
                }
                continue;
            case 7: // "animate".length
                if (nodeName == "animate") {
                    break;
                }
                continue;
            case 12: // "animateColor".length
                if (nodeName == "animateColor") {
                    break;
                }
                continue;
            case 13: // "animateMotion".length
                if (nodeName == "animateMotion") {
                    break;
                }
                continue;
            case 16: // "animateTransform".length
                if (nodeName == "animateTransform") {
                    break;
                }
                continue;
            default:
                continue;
        }

        // deal with external timesheets
        if (src && src.length > 0) {
            if (!request) {
                // lazy initialization of XHR
                request = window.XMLHttpRequest ? new XMLHttpRequest() : window.ActiveXObject ? new ActiveXObject("MSXML2.XMLHTTP.3.0") : null;
                if (request) {
                    if (request.overrideMimeType)
                        request.overrideMimeType('text/xml');
                    request.onreadystatechange = xhrCallback;
                }
            }
            if (request) {
                request.open("GET", src, false);
                request.send(null);
            } else if (window.getURL && window.parseXML) {
                getURL(src, getURLCallback);
            }
            // reset variable
            src = null;
            continue;
        }

        // deal with animations
        if (ns2proc[anim.namespaceURI]) {
            var targets = getTargets(anim);
            var elAnimators = new Array();
            for (var k = 0; k < targets.length; ++k) {
                var target = targets[k];
                var animator = new Animator(anim, target, k);
                animators.push(animator);
                elAnimators[k] = animator;
            }
            anim.animators = elAnimators;
            var id = anim.getAttribute("id");
            if (id)
                id2anim[id] = anim;
        }
    }
}

function getTargets(anim) {
    if (anim.hasAttribute("select"))
        return select(anim);
    var href = anim.getAttributeNS(xlinkns, "href");
    if (href != null && href != "")
        return [document.getElementById(href.substring(1))];
    else {
        var target = anim.parentNode;
        if (target.localName == "item" && (target.namespaceURI == timesheetns || target.namespaceURI == smil3ns))
            return select(target);
        return [target];
    }
}

function select(element) {
    var selector = element.getAttribute("select");
    var parent = element.parentNode;
    while (parent && parent.nodeType == 1) {
        if (parent.localName == "item" && (parent.namespaceURI == timesheetns || parent.namespaceURI == smil3ns))
            selector = parent.getAttribute("select") + " " + selector;
        parent = parent.parentNode;
    }
    return document.querySelectorAll(selector);
}

function getEventTargetsById(id, ref) {
    var element = null;
    if (id == "prev") {
        element = ref.previousSibling;
        while (element && element.nodeType != 1)
            element = element.previousSibling;
    }
    if (element == null)
        element = document.getElementById(id);
    if (element == null)
        element = id2anim[id]; // because getElementById doesn't returns SMIL elements in Gecko
    if (element == null)
        return null;
    if (element.animators)
        return element.animators;
    return [element];
}


/**
 * Corresponds to one <animate>, <set>, <animateTransform>, ...
 * (there can be more than one Animator for each element)
 */
Animator.prototype = {

    /**
	 * Registers the animation.
	 * It schedules the beginnings and endings.
	 */
    register: function () {
        var begin = this.anim.getAttribute("begin");
        if (begin)
            this.schedule(begin, this.begin);
        else
            this.begin(0);
        var end = this.anim.getAttribute("end");
        if (end)
            this.schedule(end, this.finish);
    },

    /**
	 * Schedules the starts or ends of the animation.
	 */
    schedule: function (timeValueList, func) {
        var me = this; // I do that because if I use "this", the addEventListener understands the event source
        var timeValues = timeValueList.split(";");
        for (var i = 0; i < timeValues.length; ++i) {
            var time = timeValues[i].trim();
            if (time.length > 11 && time.substring(0, 10) == "wallclock(") {
                var wallclock = new Date();
                wallclock.setISO8601(time.substring(10, time.length - 1));
                if (!isNaN(wallclock.getTime())) {
                    var now = new Date();
                    var diff = wallclock - now;
                    func.call(me, diff);
                }
            } else if (isNaN(parseInt(time))) {
                var offset = 0;
                var io = time.indexOf("+");
                if (io == -1)
                    io = time.indexOf("-");
                if (io != -1) {
                    offset = toMillis(time.substring(io).replace(/ /g, ""));
                    time = time.substring(0, io).trim();
                }
                io = time.indexOf(".");
                var elements;
                if (io == -1) {
                    elements = [this.target];
                } else {
                    var id = time.substring(0, io);
                    if (id.indexOf("index(") == 0)
                        id = id.substring(6, id.length - 1) + this.index;
                    elements = getEventTargetsById(id, this.anim);
                }
                var event = time.substring(io + 1);
                var call = funk(func, me, offset);
                for (var j = 0; j < elements.length; ++j) {
                    var element = elements[j];
                    if (element == null)
                        continue;
                    element.addEventListener(event, call, false);
                }
            } else {
                time = toMillis(time);
                func.call(me, time);
            }
        }
    },

    /**
	 * Remembers the initial value of the animated attribute.
	 * This function is overridden.
	 */
    getCurVal: function () {
        if (this.attributeType == "CSS") {
            // should use this.target.getPresentationAttribute instead
            return this.target.style.getPropertyValue(this.attributeName);
        } else {
            //var animAtt = this.target[this.attributeName];
            //if (animAtt && animAtt.animVal)
            //	return animAtt.animVal.value;
            //else
            return this.target.getAttributeNS(this.namespace, this.attributeName);
        }
    },

    /**
	 * Starts the animation.
	 * I mean the very beginning of it.
	 * Not called when repeating.
	 */
    begin: function (offset) {
        if (this.restart == "never" || (this.running && this.restart == "whenNotActive"))
            return;
        if (this.running)
            this.finish();
        if (offset && offset > 0) {
            var me = this;
            var myself = this.begin;
            var call = function () { myself.call(me) };
            window.setTimeout(call, offset);
            return;
        }
        this.startTime = new Date();
        if (offset && offset < 0) {
            this.startTime.setTime(this.startTime.getTime() + offset);
            if (this.startTime < timeZero)
                return;
        }
        this.stop();
        this.running = true;
        var initVal = this.getCurVal();
        this.realInitVal = initVal;
        // TODO
        // I should get the inherited value here (getPresentationAttribute is not supported)
        if (!initVal && propDefaults[this.attributeName])
            initVal = propDefaults[this.attributeName];
        if (this.anim.nodeName == "set")
            this.step(this.to);
        this.iteration = 0;

        if (this.values) {
            this.animVals = this.values.split(";");
            for (var i = 0; i < this.animVals.length; ++i)
                this.animVals[i] = this.animVals[i].trim();
        } else {
            this.animVals = new Array();
            if (this.from)
                this.animVals[0] = this.from;
            else
                this.animVals[0] = initVal;
            if (this.by && this.animVals[0])
                this.animVals[1] = this.add(this.normalize(this.animVals[0]), this.normalize(this.by));
            else
                this.animVals[1] = this.to;
        }
        if (this.animVals[this.animVals.length - 1]) {
            this.freezed = this.animVals[this.animVals.length - 1];

            if (this.animVals[0]) {
                if ((this.animVals[0][0] == "#" || colors[this.animVals[0]] || (this.animVals[0].length > 5 && this.animVals[0].trim().substring(0, 4) == "rgb(")) &&
					 (this.freezed[0] == "#" || colors[this.freezed] || (this.freezed.length > 5 && this.freezed.trim().substring(0, 4) == "rgb(")))
                    this.color();
                else {
                    var cp = new Array();
                    var oneVal = this.animVals[0];
                    var qualified = getUnit(oneVal);
                    cp[0] = qualified[0];
                    this.unit = qualified[1];
                    for (var i = 1; i < this.animVals.length; ++i) {
                        var oneVal = this.animVals[i];
                        var qualified = getUnit(oneVal);
                        if (qualified[1] == this.unit)
                            cp[i] = qualified[0];
                        else {
                            cp = this.animVals;
                            break;
                        }
                    }
                    this.animVals = cp;
                }
            }
        }

        this.iterBegin = this.startTime;
        animations.push(this);
        // if this is the first running animator, start the rendering loop
        if (!animTimer) {
            // asynchronous to render all animators, listeners, etc. starting at this frame
            window.setTimeout(animate, 0);
            // schedules the rendering loop
            animTimer = window.setInterval(animate, mpf);
        }
        for (var i = 0; i < this.beginListeners.length; ++i)
            this.beginListeners[i].call();
        var onbegin = this.anim.getAttribute("onbegin");
        if (onbegin)
            eval(onbegin);
    },

    /**
	 * This function is overridden for multiple values attributes (scale, rotate, translate).
	 */
    normalize: function (value) {
        return value;
    },

    /**
	 * Sums up two normalized values.
	 */
    add: function (a, b) {
        return "" + (parseFloat(a) + parseFloat(b));
    },

    /**
	 * Computes and applies the animated value for a given time.
	 * Returns false if this animation has been stopped (removed from the running array).
	 */
    f: function (curTime) {
        var dur = this.computedDur;
        if (isNaN(dur))
            return true;

        var beginTime = this.iterBegin;
        var diff = curTime - beginTime;
        var percent = diff / dur;
        if (percent >= 1)
            return this.end(curTime);

        var iteration = this.iteration;
        if (this.repeatCount && this.repeatCount != "indefinite" && (iteration + percent) >= this.repeatCount) {
            if (this.fill == "freeze")
                this.freezed = this.valueAt(this.repeatCount - iteration);
            return this.end(curTime);
        }
        if (this.repeatDur && this.repeatDur != "indefinite" && (curTime - this.startTime) >= toMillis(this.repeatDur)) {
            if (this.fill == "freeze") {
                var div = toMillis(this.repeatDur) / dur;
                this.freezed = this.valueAt(div - Math.floor(div));
            }
            return this.end(curTime);
        }

        var anim = this.anim;
        if (anim.localName == "set")
            return true;

        var curVal = this.valueAt(percent);
        this.step(curVal);
        return true;
    },

    isInterpolable: function (from, to) {
        var areN = (!isNaN(from) && !isNaN(to));
        if (!areN && from.trim().indexOf(" ") != -1 && to.trim().indexOf(" ") != -1) {
            var tfrom = from.trim().split(" ");
            var tto = to.trim().split(" ");
            areN = true;
            if (tfrom.length == tto.length)
                for (var i = 0; i < tto.length; ++i)
                    if (!this.isInterpolable(tfrom[i], tto[i]))
                        return false;
        }
        return areN;
    },

    valueAt: function (percent) {
        var tValues = this.animVals;
        if (percent == 1)
            return tValues[tValues.length - 1];
        if (this.calcMode == "discrete" || !this.isInterpolable(tValues[0], tValues[1])) {
            if (this.keyTimes) {
                for (var i = 1; i < this.keyTimes.length; ++i)
                    if (this.keyTimes[i] > percent)
                        return tValues[i - 1];
                return tValues[tValues.length - 1];
            }
            var parts = tValues.length;
            var div = Math.floor(percent * parts);
            return tValues[div];
        } else {
            var index;
            if (this.keyTimes) {
                for (var i = 1; i < this.keyTimes.length; ++i)
                    if (this.keyTimes[i] > percent) {
                        index = i - 1;
                        var t1 = this.keyTimes[index];
                        percent = (percent - t1) / (this.keyTimes[i] - t1);
                        break;
                    }
                if (i >= this.keyTimes.length)
                    index = i - 2;
            } else {
                var parts = tValues.length - 1;
                index = Math.floor(percent * parts);
                percent = (percent % (1 / parts)) * parts;
            }
            if (this.calcMode == "spline")
                percent = this.spline(percent, index);
            return this.interpolate(this.normalize(tValues[index]), this.normalize(tValues[index + 1]), percent);
        }
    },

    spline: function (percent, index) {
        var path = this.keySplines[index];
        var tot = path.getTotalLength();
        var step = tot / splinePrecision;
        for (var i = 0; i <= tot; i += step) {
            var pt = path.getPointAtLength(i);
            if (pt.x > percent) {
                var pt1 = path.getPointAtLength(i - step);
                percent -= pt1.x;
                percent /= pt.x - pt1.x;
                return pt1.y + ((pt.y - pt1.y) * percent);
            }
        }
        var pt = path.getPointAtLength(tot);
        var pt1 = path.getPointAtLength(tot - step);
        percent -= pt1.x;
        percent /= pt.x - pt1.x;
        return pt1.y + ((pt.y - pt1.y) * percent);
    },

    /**
	 * Performs the interpolation.
	 * This function is overridden.
	 */
    interpolate: function (from, to, percent) {
        if (!this.isInterpolable(from, to)) {
            if (percent < .5)
                return from;
            else
                return to;
        }
        if (from.trim().indexOf(" ") != -1) {
            var tfrom = from.split(" ");
            var tto = to.split(" ");
            var ret = new Array();
            for (var i = 0; i < tto.length; ++i)
                ret[i] = parseFloat(tfrom[i]) + ((tto[i] - tfrom[i]) * percent);
            return ret.join(" ");
        }
        return parseFloat(from) + ((to - from) * percent);
    },

    /**
	 * Apply a value to the attribute the animator is linked to.
	 * This function is overridden.
	 */
    step: function (value) {
        var attributeName = this.attributeName;
        var attributeType = this.attributeType;
        if (attributeType == "CSS") {
            // workaround a Gecko and WebKit bug
            if (attributeName == "font-size" && !isNaN(value))
                value += "px";
            else if (this.unit)
                value += this.unit;
            this.target.style.setProperty(attributeName, value, "");
        } else {
            if (this.unit)
                value += this.unit;
            //var animAtt = this.target[attributeName];
            //if (animAtt && animAtt.animVal)
            //	animAtt.animVal.value = value;
            //else
            this.target.setAttributeNS(this.namespace, attributeName, value);
        }
    },

    /**
	 * Normal end of the animation:
	 * it restarts if repeatCount.
	 */
    end: function (now) {
        if (!this.repeatCount && !this.repeatDur)
            return this.finish();
        else {
            ++this.iteration;
            if (this.repeatCount && this.repeatCount != "indefinite" && this.iteration >= this.repeatCount)
                return this.finish();
            else if (this.repeatDur && this.repeatDur != "indefinite" && (now - this.startTime) >= toMillis(this.repeatDur))
                return this.finish();
            else {
                if (this.accumulate == "sum") {
                    var curVal = this.getCurVal();
                    if (!curVal && propDefaults[this.attributeName])
                        curVal = propDefaults[this.attributeName];

                    if (this.by && !this.from) {
                        this.animVals[0] = curVal;
                        this.animVals[1] = this.add(this.normalize(curVal), this.normalize(this.by));
                    } else {
                        for (var i = 0; i < this.animVals.length; ++i)
                            this.animVals[i] = this.add(this.normalize(curVal), this.normalize(this.animVals[i]));
                    }
                    this.freezed = this.animVals[this.animVals.length - 1];
                }
                this.iterBegin = now;
                for (var i = 0; i < this.repeatIterations.length; ++i) {
                    if (this.repeatIterations[i] == this.iteration)
                        this.repeatListeners[i].call();
                }
                var onrepeat = this.anim.getAttribute("onrepeat");
                if (onrepeat)
                    eval(onrepeat);
            }
        }
        return true;
    },

    /**
	 * Really stop of the animation (it doesn't repeat).
	 * Freezes or removes the animated value.
	 */
    finish: function (offset) {
        if (this.min && this.min != "indefinite") {
            var now = new Date();
            if ((now - this.startTime) >= this.computedMin)
                return true;
        }
        if (offset && offset > 0) {
            var me = this;
            var myself = this.finish;
            var call = function () { myself.call(me) };
            window.setTimeout(call, offset);
            return true;
        }
        if (offset && offset < 0) {
            var now = new Date();
            now.setTime(now.getTime() + offset);
            if (now < this.startTime)
                return true;
        }

        var fill = this.fill;
        var kept = true;
        if (fill == "freeze") {
            this.freeze();
        } else {
            this.stop();
            this.step(this.realInitVal);
            kept = false;
        }
        if (this.running) {
            for (var i = 0; i < this.endListeners.length; ++i)
                this.endListeners[i].call();
            var onend = this.anim.getAttribute("onend");
            if (onend)
                eval(onend);
            this.running = false;
        }
        return kept;
    },

    /**
	 * Removes this animation from the running array.
	 */
    stop: function () {
        for (var i = 0, j = animations.length; i < j; ++i)
            if (animations[i] == this) {
                animations.splice(i, 1);
                // if this is the last running animator, stop the rendering loop
                if (!animations.length && animTimer) {
                    window.clearInterval(animTimer);
                    animTimer = null;
                }
                break;
            }
    },

    /**
	 * Freezes the attribute value to the ending value.
	 */
    freeze: function () {
        this.step(this.freezed);
    },

    /**
	 * Adds a listener to this animation beginning or ending.
	 */
    addEventListener: function (event, func, b) {
        if (event == "begin")
            this.beginListeners.push(func);
        else if (event == "end")
            this.endListeners.push(func);
        else if (event.length > 7 && event.substring(0, 6) == "repeat") {
            var iteration = event.substring(7, event.length - 1);
            this.repeatListeners.push(func);
            this.repeatIterations.push(iteration);
        }
    },

    /**
	 * Returns the path linked to this animateMotion.
	 */
    getPath: function () {
        var mpath = this.anim.getElementsByTagNameNS(svgns, "mpath")[0];
        if (mpath) {
            var pathHref = mpath.getAttributeNS(xlinkns, "href");
            return document.getElementById(pathHref.substring(1));
        } else {
            var d = this.anim.getAttribute("path");
            if (d) {
                var pathEl = createPath(d);
                //pathEl.setAttribute("display", "none");
                //this.anim.parentNode.appendChild(pathEl);
                return pathEl;
            }
        }
        return null;
    },

    /**
	 * Initializes this animator as a translation (x,y):
	 * <animateTransform type="translate"> or
	 * <animateMotion> without a path.
	 */
    translation: function () {
        if (this.by && this.by.indexOf(",") == -1)
            this.by = this.by + ",0";
        this.normalize = function (value) {
            var coords = value.replace(/,/g, " ").replace(/ +/, " ").split(/ /);
            coords[0] = parseFloat(coords[0]);
            if (coords.length == 1)
                coords[1] = 0;
                //coords[1] = this.initVal.split(",")[1];
            else
                coords[1] = parseFloat(coords[1]);
            return coords;
        };
        this.add = function (a, b) {
            var x = a[0] + b[0];
            var y = a[1] + b[1];
            return x + "," + y;
        };
        this.isInterpolable = function (from, to) { return true; };
        this.interpolate = function (from, to, percent) {
            var x = from[0] + ((to[0] - from[0]) * percent);
            var y = from[1] + ((to[1] - from[1]) * percent);
            return x + "," + y;
        };
    },

    /**
	 * Initializes this animator as a color animation:
	 * <animateColor> or
	 * <animate> on a color attribute.
	 */
    color: function () {
        this.isInterpolable = function (from, to) { return true; };
        this.interpolate = function (from, to, percent) {
            var r = Math.round(from[0] + ((to[0] - from[0]) * percent));
            var g = Math.round(from[1] + ((to[1] - from[1]) * percent));
            var b = Math.round(from[2] + ((to[2] - from[2]) * percent));
            var val = "rgb(" + r + "," + g + "," + b + ")";
            return val;
        };
        this.normalize = function (value) {
            var rgb = toRGB(value);
            if (rgb == null)
                return toRGB(propDefaults[this.attributeName]);
            return rgb;
        };
        this.add = function (a, b) {
            var ret = new Array();
            for (var i = 0; i < a.length; ++i)
                ret.push(Math.min(a[i], 255) + Math.min(b[i], 255));
            return ret.join(",");
        };
    },

    d: function () {
        this.isInterpolable = function (from, to) { return true; };
        this.interpolate = function (from, to, percent) {
            var path = "";
            var listFrom = from.myNormalizedPathSegList;
            var listTo = to.myNormalizedPathSegList;
            var segFrom, segTo, typeFrom, typeTo;
            for (var i = 0, j = Math.min(listFrom.numberOfItems, listTo.numberOfItems) ; i < j; ++i) {
                segFrom = listFrom.getItem(i);
                segTo = listTo.getItem(i);
                typeFrom = segFrom.pathSegType;
                typeTo = segTo.pathSegType;
                // NOTE: in 'normalizedPathSegList', only 'M', 'L', 'C' and 'z' path data commands are expected
                if (typeFrom == 1 || typeTo == 1) // PATHSEG_CLOSEPATH
                    path += " z ";
                else {
                    var x = segFrom.x + ((segTo.x - segFrom.x) * percent);
                    var y = segFrom.y + ((segTo.y - segFrom.y) * percent);
                    if (typeFrom == 2 || typeTo == 2) // PATHSEG_MOVETO_ABS
                        path += " M ";
                    else if (typeFrom == 4 || typeTo == 4) // PATHSEG_LINETO_ABS
                        path += " L ";
                        // NOTE: need to be more strict here, as interpolating a 'C' command with an 'M' or an 'L' isn't yet supported
                        // (additional trickery is required for dealing with different DOM interfaces and interpolating them)
                    else if (typeFrom == 6 && typeTo == 6) { // PATHSEG_CURVETO_CUBIC_ABS
                        var x1 = segFrom.x1 + ((segTo.x1 - segFrom.x1) * percent);
                        var y1 = segFrom.y1 + ((segTo.y1 - segFrom.y1) * percent);
                        var x2 = segFrom.x2 + ((segTo.x2 - segFrom.x2) * percent);
                        var y2 = segFrom.y2 + ((segTo.y2 - segFrom.y2) * percent);
                        path += " C " + x1 + "," + y1 + " " + x2 + "," + y2 + " ";
                    } else
                        // "unexpected" type found, which means that 'pathSegList' is being used
                        // (incomplete support for segment interpolation therefore switch to a discrete approach)
                        return (percent < .5 ? from : to).getAttribute("d");
                    path += x + "," + y;
                }
            }
            return path;
        };
        this.normalize = function (value) {
            var path = createPath(value);
            return path;
        };
    }

};

/**
 * Constructor:
 * - initializes
 * - gets the attributes
 * - corrects and precomputes some values
 * - specializes some functions
 */
function Animator(anim, target, index) {
    this.anim = anim;
    this.target = target;
    this.index = index;
    anim.targetElement = target;
    this.attributeType = anim.getAttribute("attributeType");
    this.attributeName = anim.getAttribute("attributeName");
    if (this.attributeType != "CSS" && this.attributeType != "XML") {
        // attributeType not specified, default stands for "auto"
        // "The implementation must first search through the list of CSS properties for a matching property name"
        // http://www.w3.org/TR/SVG11/animate.html#AttributeTypeAttribute
        if (propDefaults[this.attributeName] && this.target.style.getPropertyValue(this.attributeName))
            this.attributeType = "CSS";
        else
            this.attributeType = "XML";
    }
    if (this.attributeType == "XML" && this.attributeName) {
        this.namespace = null;
        var chColon = this.attributeName.indexOf(":");
        if (chColon != -1) {
            var prefix = this.attributeName.substring(0, chColon);
            this.attributeName = this.attributeName.substring(chColon + 1);
            var node = target;
            while (node && node.nodeType == 1) {
                var ns = node.getAttributeNS("http://www.w3.org/2000/xmlns/", prefix);
                if (ns) {
                    this.namespace = ns;
                    break;
                }
                node = node.parentNode;
            }
        }
    }

    if (this.attributeName == "d")
        this.d();
    else if (this.attributeName == "points") {
        this.isInterpolable = function (from, to) { return true; };
        this.interpolate = function (from, to, percent) {
            var ret = new Array();
            var xyFrom, xyTo, x, y;
            for (var i = 0, j = Math.min(from.length, to.length) ; i < j; ++i) {
                xyFrom = from[i].split(",");
                xyTo = to[i].split(",");
                x = parseFloat(xyFrom[0]) + ((parseFloat(xyTo[0]) - xyFrom[0]) * percent);
                y = parseFloat(xyFrom[1]) + ((parseFloat(xyTo[1]) - xyFrom[1]) * percent);
                ret.push(x + "," + y);
            }
            return ret.join(" ");
        };
        this.normalize = function (value) {
            var ar = value.split(" ");
            for (var i = ar.length - 1; i >= 0; --i)
                if (ar[i] == "")
                    ar.splice(i, 1);
            return ar;
        };
    }
    this.from = anim.getAttribute("from");
    this.to = anim.getAttribute("to");
    this.by = anim.getAttribute("by");
    this.values = anim.getAttribute("values");
    if (this.values) {
        this.values = this.values.trim();
        if (this.values[this.values.length - 1] == ";")
            this.values = this.values.substring(0, this.values.length - 1);
    }
    this.calcMode = anim.getAttribute("calcMode");
    this.keyTimes = anim.getAttribute("keyTimes");
    if (this.keyTimes) {
        this.keyTimes = this.keyTimes.split(";");
        for (var i = 0; i < this.keyTimes.length; ++i)
            this.keyTimes[i] = parseFloat(this.keyTimes[i]);
        this.keyPoints = anim.getAttribute("keyPoints");
        if (this.keyPoints) {
            this.keyPoints = this.keyPoints.split(";");
            for (var i = 0; i < this.keyPoints.length; ++i)
                this.keyPoints[i] = parseFloat(this.keyPoints[i]);
        }
    }
    this.keySplines = anim.getAttribute("keySplines");
    if (this.keySplines) {
        this.keySplines = this.keySplines.split(";");
        for (var i = 0; i < this.keySplines.length; ++i)
            this.keySplines[i] = createPath("M 0 0 C " + this.keySplines[i] + " 1 1");
    }
    this.dur = anim.getAttribute("dur");
    if (this.dur && this.dur != "indefinite")
        this.computedDur = toMillis(this.dur);
    this.max = anim.getAttribute("max");
    if (this.max && this.max != "indefinite") {
        this.computedMax = toMillis(this.max);
        if (!isNaN(this.computedMax) && this.computedMax > 0 && (!this.computedDur || this.computedDur > this.computedMax))
            this.computedDur = this.computedMax;
    }
    this.min = anim.getAttribute("min");
    if (this.min) {
        this.computedMin = toMillis(this.min);
        if (!this.computedDur || this.computedDur < this.computedMin)
            this.computedDur = this.computedMin;
    }

    this.fill = anim.getAttribute("fill");
    this.type = anim.getAttribute("type");
    this.repeatCount = anim.getAttribute("repeatCount");
    this.repeatDur = anim.getAttribute("repeatDur");
    this.accumulate = anim.getAttribute("accumulate");
    this.additive = anim.getAttribute("additive");
    this.restart = anim.getAttribute("restart");
    if (!this.restart)
        this.restart = "always";

    this.beginListeners = new Array();
    this.endListeners = new Array();
    this.repeatListeners = new Array();
    this.repeatIterations = new Array();

    var nodeName = anim.localName;

    if (nodeName == "animateColor") {

        this.color();

    } else if (nodeName == "animateMotion") {

        this.isInterpolable = function (from, to) { return true; };
        this.getCurVal = function () {
            var curTrans = this.target.transform;
            if (curTrans && curTrans.animVal.numberOfItems > 0) {
                var transList = curTrans.animVal;
                return decompose(transList.getItem(0).matrix, "translate");
            } else
                return "0,0";
        };
        this.path = this.getPath();
        if (this.path) {
            this.valueAt = function (percent) {
                var length = this.path.getTotalLength();
                var point = this.path.getPointAtLength(percent * length);
                return point.x + "," + point.y;
            };
        } else {
            this.translation();
        }
        this.freeze = function () {
            var val = this.valueAt(1);
            this.step(val);
        };
        if (this.keyPoints && this.keyTimes) {
            this.pathKeyTimes = this.keyTimes;
            this.keyTimes = null;
            this.superValueAt = this.valueAt;
            this.valueAt = function (percent) {
                for (var i = 1; i < this.keyPoints.length; ++i) {
                    var fakePC = this.keyPoints[this.keyPoints.length - 1]
                    if (this.pathKeyTimes[i] > percent) {
                        var pt = this.keyPoints[i - 1];
                        if (this.calcMode == "discrete")
                            fakePC = pt;
                        else {
                            var t1 = this.pathKeyTimes[i - 1];
                            percent = (percent - t1) / (this.pathKeyTimes[i] - t1);
                            fakePC = pt + ((this.keyPoints[i] - pt) * percent)
                        }
                        break;
                    }
                }
                return this.superValueAt(fakePC);
            };
        }
        this.step = function (value) {
            value = "translate(" + value + ")";
            this.target.setAttribute("transform", value);
        };

    } else if (nodeName == "animateTransform") {

        this.isInterpolable = function (from, to) { return true; };
        this.getCurVal = function () {
            var type = this.type;
            var curTrans = this.target.transform;
            if (curTrans && curTrans.animVal.numberOfItems > 0) {
                var transList = curTrans.animVal;
                return decompose(transList.getItem(0).matrix, type);
            } else {
                if (type == "scale")
                    return "1,1";
                else if (type == "translate")
                    return "0,0";
                else if (type == "rotate")
                    return "0,0,0";
                else
                    return 0;
            }
        };

        if (this.type == "scale") {
            this.normalize = function (value) {
                value = value.replace(/,/g, " ");
                var coords = value.split(" ");
                coords[0] = parseFloat(coords[0]);
                if (coords.length == 1)
                    coords[1] = coords[0];
                else
                    coords[1] = parseFloat(coords[1]);
                return coords;
            };
            this.add = function (a, b) {
                var ret = new Array();
                for (var i = 0; i < a.length; ++i)
                    ret.push(a[i] * b[i]);
                return ret.join(",");
            };
        } else if (this.type == "translate") {
            this.translation();
        } else if (this.type == "rotate") {
            this.normalize = function (value) {
                value = value.replace(/,/g, " ");
                var coords = value.split(" ");
                coords[0] = parseFloat(coords[0]);
                if (coords.length < 3) {
                    coords[1] = 0;
                    coords[2] = 0;
                } else {
                    coords[1] = parseFloat(coords[1]);
                    coords[2] = parseFloat(coords[2]);
                }
                return coords;
            };
            this.add = function (a, b) {
                var ret = new Array();
                for (var i = 0; i < a.length; ++i)
                    ret.push(a[i] + b[i]);
                return ret.join(",");
            };
        }

        if (this.type == "scale" || this.type == "rotate") {
            if (this.from)
                this.from = this.normalize(this.from).join(",");
            if (this.to)
                this.to = this.normalize(this.to).join(",");
            if (this.by)
                this.by = this.normalize(this.by).join(",");
            if (this.values) {
                var tvals = this.values.split(";");
                for (var i = 0; i < tvals.length; ++i)
                    tvals[i] = this.normalize(tvals[i]).join(",");
                this.values = tvals.join(";");
            }
            this.interpolate = function (from, to, percent) {
                var ret = new Array();
                for (var i = 0; i < from.length; ++i)
                    ret.push(from[i] + ((to[i] - from[i]) * percent));
                return ret.join(",");
            };
        }

        this.step = function (value) {
            var attributeName = this.attributeName;
            value = this.type + "(" + value + ")";
            this.target.setAttribute(attributeName, value);
        };
    }

    var me = this;
    this.anim.beginElement = function () { me.begin(); return true; };
    this.anim.beginElementAt = function (offset) { me.begin(offset * 1000); return true; };
    this.anim.endElement = function () { me.finish(); return true; };
    this.anim.endElementAt = function (offset) { me.finish(offset * 1000); return true; };

    this.anim.getStartTime = function () { return (me.iterBegin - timeZero) / 1000; };
    this.anim.getCurrentTime = function () {
        var now = new Date();
        return (now - me.iterBegin) / 1000;
    };
}


/**
 * Can be called at any time.
 * It's the main loop.
 */
function animate() {
    var curTime = new Date();
    if (curTime <= prevTime)
        return;
    for (var i = 0, j = animations.length; i < j; ++i) {
        try {
            if (!animations[i].f(curTime)) {
                // animation was removed therefore we need to adjust both the iterator and the auxiliary variable
                --i; --j;
            }
        } catch (exc) {
            if (exc.message !== "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIDOMSVGPathElement.getTotalLength]") {
                // NOTE: in IE, console object is only available when Developer tools are open
                if (window.console && console.log) {
                    console.log(exc);
                    // uncomment to force error display
                    //} else {
                    //	alert(exc);
                }
            }
        }
    }
    prevTime = curTime;
    // it would be cool if the attributes would be computed only, in the previous loop
    // and then the last values applied after the loop
    // for that, f(t) must return the value, and we must have a map for object(?).attributeType.attributeName -> value
    // then f(t) cannot return false when autostopping -> we must find another mechanism
}


/**
 * Converts a clock-value to milliseconds.
 * Supported: "s" | "ms" | "min" | "h" | no-units
 */
function toMillis(time) {
    time = time.trim();
    var len = time.length;
    var io = time.indexOf(":");

    if (io != -1) {
        var clockVal = time.split(":");
        len = clockVal.length;
        time = 0;
        if (len == 3)
            time += parseInt(clockVal[0]) * 3600000;
        time += parseInt(clockVal[len - 2]) * 60000;
        time += parseFloat(clockVal[len - 1]) * 1000;
    } else if (len > 2 && time.substring(len - 2) == "ms") {
        time = parseInt(time.substring(0, time.length - 2));
    } else if (len > 1 && time[len - 1] == "s") {
        time = time.substring(0, time.length - 1);
        time *= 1000;
    } else if (len > 3 && time.substring(len - 3) == "min") {
        time = time.substring(0, time.length - 3);
        time *= 60000;
    } else if (len > 1 && time[len - 1] == "h") {
        time = time.substring(0, time.length - 1);
        time *= 3600000;
    } else {
        time *= 1000;
    }
    return time;
}


/**
 * Decompose a matrix into its scale, translate, rotate or skew.
 */
function decompose(matrix, type) {
    if (type == "translate")
        return matrix.e + "," + matrix.f;

    var a = matrix.a;
    var b = matrix.b;
    var c = matrix.c;
    var d = matrix.d;

    if (type == "rotate")
        return Math.atan2(c, a) + ",0,0";

    var ModA = Math.sqrt(a * a + c * c);
    var ModB = Math.sqrt(b * b + d * d);

    if (type == "scale") {
        var AxB = a * d - b * c;
        var scaleX = AxB == 0 ? 0 : (AxB / ModA);
        var scaleY = ModB;
        return scaleX + "," + scaleY;
    }
    var AdotB = a * b + c * d;
    if (AdotB == 0)
        return 0;
    var shear = Math.PI / 2 - Math.acos(AdotB / (ModB * ModA));
    return (shear * 180) / Math.PI;
}


/**
 * Convert an rgb(), #XXX, #XXXXXX or named color
 * into an [r,g,b] array.
 */
function toRGB(color) {
    if (color.substring(0, 3) == "rgb") {
        color = color.replace(/ /g, "");
        color = color.replace("rgb(", "");
        color = color.replace(")", "");
        var rgb = color.split(",");
        for (var i = 0; i < rgb.length; ++i) {
            var len = rgb[i].length - 1;
            if (rgb[i][len] == "%")
                rgb[i] = Math.round((rgb[i].substring(0, len)) * 2.55);
            else
                rgb[i] = parseInt(rgb[i]);
        }
        return rgb;
    } else if (color.charAt(0) == "#") {
        color = color.trim();
        var rgb = new Array();
        if (color.length == 7) {
            rgb[0] = parseInt(color.substring(1, 3), 16);
            rgb[1] = parseInt(color.substring(3, 5), 16);
            rgb[2] = parseInt(color.substring(5, 7), 16);
        } else {
            rgb[0] = color.substring(1, 2);
            rgb[1] = color.substring(2, 3);
            rgb[2] = color.substring(3, 4);
            rgb[0] = parseInt(rgb[0] + rgb[0], 16);
            rgb[1] = parseInt(rgb[1] + rgb[1], 16);
            rgb[2] = parseInt(rgb[2] + rgb[2], 16);
        }
        return rgb;
    } else {
        return colors[color];
    }
}


function createPath(d) {
    var path = document.createElementNS(svgns, "path");
    path.setAttribute("d", d);
    try {
        if (path.normalizedPathSegList)
            path.myNormalizedPathSegList = path.normalizedPathSegList;
    } catch (exc) { }
    if (!path.myNormalizedPathSegList) {
        // TODO : normalize the path
        path.myNormalizedPathSegList = path.pathSegList;
    }
    return path;
}


// NOTE: units which aren't valid variable names are enclosed in quotes
var units = { grad: 1, deg: 1, rad: 1, kHz: 1, Hz: 1, em: 1, ex: 1, px: 1, pt: 1, pc: 1, mm: 1, cm: 1, in: 1, ms: 1, s: 1, "%": 1 };
function getUnit(str) {
    if (str && str.substring && str.length > 1) {
        for (var i = 1; i < 4; ++i) { // loop through units string length
            var vlen = str.length - i;
            if (vlen > 0) {
                var unit = str.substring(vlen);
                if (units[unit]) {
                    var val = str.substring(0, vlen);
                    if (!isNaN(val))
                        return [val, unit];
                }
            }
        }
    }
    return [str, null];
}

var colors = {
    aliceblue: [240, 248, 255],
    antiquewhite: [250, 235, 215],
    aqua: [0, 255, 255],
    aquamarine: [127, 255, 212],
    azure: [240, 255, 255],
    beige: [245, 245, 220],
    bisque: [255, 228, 196],
    black: [0, 0, 0],
    blanchedalmond: [255, 235, 205],
    blue: [0, 0, 255],
    blueviolet: [138, 43, 226],
    brown: [165, 42, 42],
    burlywood: [222, 184, 135],
    cadetblue: [95, 158, 160],
    chartreuse: [127, 255, 0],
    chocolate: [210, 105, 30],
    coral: [255, 127, 80],
    cornflowerblue: [100, 149, 237],
    cornsilk: [255, 248, 220],
    crimson: [220, 20, 60],
    cyan: [0, 255, 255],
    darkblue: [0, 0, 139],
    darkcyan: [0, 139, 139],
    darkgoldenrod: [184, 134, 11],
    darkgray: [169, 169, 169],
    darkgreen: [0, 100, 0],
    darkgrey: [169, 169, 169],
    darkkhaki: [189, 183, 107],
    darkmagenta: [139, 0, 139],
    darkolivegreen: [85, 107, 47],
    darkorange: [255, 140, 0],
    darkorchid: [153, 50, 204],
    darkred: [139, 0, 0],
    darksalmon: [233, 150, 122],
    darkseagreen: [143, 188, 143],
    darkslateblue: [72, 61, 139],
    darkslategray: [47, 79, 79],
    darkslategrey: [47, 79, 79],
    darkturquoise: [0, 206, 209],
    darkviolet: [148, 0, 211],
    deeppink: [255, 20, 147],
    deepskyblue: [0, 191, 255],
    dimgray: [105, 105, 105],
    dimgrey: [105, 105, 105],
    dodgerblue: [30, 144, 255],
    firebrick: [178, 34, 34],
    floralwhite: [255, 250, 240],
    forestgreen: [34, 139, 34],
    fuchsia: [255, 0, 255],
    gainsboro: [220, 220, 220],
    ghostwhite: [248, 248, 255],
    gold: [255, 215, 0],
    goldenrod: [218, 165, 32],
    gray: [128, 128, 128],
    grey: [128, 128, 128],
    green: [0, 128, 0],
    greenyellow: [173, 255, 47],
    honeydew: [240, 255, 240],
    hotpink: [255, 105, 180],
    indianred: [205, 92, 92],
    indigo: [75, 0, 130],
    ivory: [255, 255, 240],
    khaki: [240, 230, 140],
    lavender: [230, 230, 250],
    lavenderblush: [255, 240, 245],
    lawngreen: [124, 252, 0],
    lemonchiffon: [255, 250, 205],
    lightblue: [173, 216, 230],
    lightcoral: [240, 128, 128],
    lightcyan: [224, 255, 255],
    lightgoldenrodyellow: [250, 250, 210],
    lightgray: [211, 211, 211],
    lightgreen: [144, 238, 144],
    lightgrey: [211, 211, 211],
    lightpink: [255, 182, 193],
    lightsalmon: [255, 160, 122],
    lightseagreen: [32, 178, 170],
    lightskyblue: [135, 206, 250],
    lightslategray: [119, 136, 153],
    lightslategrey: [119, 136, 153],
    lightsteelblue: [176, 196, 222],
    lightyellow: [255, 255, 224],
    lime: [0, 255, 0],
    limegreen: [50, 205, 50],
    linen: [250, 240, 230],
    magenta: [255, 0, 255],
    maroon: [128, 0, 0],
    mediumaquamarine: [102, 205, 170],
    mediumblue: [0, 0, 205],
    mediumorchid: [186, 85, 211],
    mediumpurple: [147, 112, 219],
    mediumseagreen: [60, 179, 113],
    mediumslateblue: [123, 104, 238],
    mediumspringgreen: [0, 250, 154],
    mediumturquoise: [72, 209, 204],
    mediumvioletred: [199, 21, 133],
    midnightblue: [25, 25, 112],
    mintcream: [245, 255, 250],
    mistyrose: [255, 228, 225],
    moccasin: [255, 228, 181],
    navajowhite: [255, 222, 173],
    navy: [0, 0, 128],
    oldlace: [253, 245, 230],
    olive: [128, 128, 0],
    olivedrab: [107, 142, 35],
    orange: [255, 165, 0],
    orangered: [255, 69, 0],
    orchid: [218, 112, 214],
    palegoldenrod: [238, 232, 170],
    palegreen: [152, 251, 152],
    paleturquoise: [175, 238, 238],
    palevioletred: [219, 112, 147],
    papayawhip: [255, 239, 213],
    peachpuff: [255, 218, 185],
    peru: [205, 133, 63],
    pink: [255, 192, 203],
    plum: [221, 160, 221],
    powderblue: [176, 224, 230],
    purple: [128, 0, 128],
    red: [255, 0, 0],
    rosybrown: [188, 143, 143],
    royalblue: [65, 105, 225],
    saddlebrown: [139, 69, 19],
    salmon: [250, 128, 114],
    sandybrown: [244, 164, 96],
    seagreen: [46, 139, 87],
    seashell: [255, 245, 238],
    sienna: [160, 82, 45],
    silver: [192, 192, 192],
    skyblue: [135, 206, 235],
    slateblue: [106, 90, 205],
    slategray: [112, 128, 144],
    slategrey: [112, 128, 144],
    snow: [255, 250, 250],
    springgreen: [0, 255, 127],
    steelblue: [70, 130, 180],
    tan: [210, 180, 140],
    teal: [0, 128, 128],
    thistle: [216, 191, 216],
    tomato: [255, 99, 71],
    turquoise: [64, 224, 208],
    violet: [238, 130, 238],
    wheat: [245, 222, 179],
    white: [255, 255, 255],
    whitesmoke: [245, 245, 245],
    yellow: [255, 255, 0],
    yellowgreen: [154, 205, 50]
};

// NOTE: variables cannot contain dashes, as they are seen as a subtraction expression
// (therefore, in those cases, enclosing in quotes is required)
var propDefaults = {
    font: "see individual properties",
    "font-family": "Arial",
    "font-size": "medium",
    "font-size-adjust": "none",
    "font-stretch": "normal",
    "font-style": "normal",
    "font-variant": "normal",
    "font-weight": "normal",
    direction: "ltr",
    "letter-spacing": "normal",
    "text-decoration": "none",
    "unicode-bidi": "normal",
    "word-spacing": "normal",
    clip: "auto",
    color: "depends on user agent",
    cursor: "auto",
    display: "inline",
    overflow: "hidden",
    visibility: "visible",
    "clip-path": "none",
    "clip-rule": "nonzero",
    mask: "none",
    opacity: 1,
    "enable-background": "accumulate",
    filter: "none",
    "flood-color": "black",
    "flood-opacity": 1,
    "lighting-color": "white",
    "stop-color": "black",
    "stop-opacity": 1,
    "pointer-events": "visiblePainted",
    "color-interpolation": "sRGB",
    "color-interpolation-filters": "linearRGB",
    "color-profile": "auto",
    "color-rendering": "auto",
    fill: "black",
    "fill-opacity": 1,
    "fill-rule": "nonzero",
    "image-rendering": "auto",
    "marker-end": "none",
    "marker-mid": "none",
    "marker-start": "none",
    "shape-rendering": "auto",
    stroke: "none",
    "stroke-dasharray": "none",
    "stroke-dashoffset": 0,
    "stroke-linecap": "butt",
    "stroke-linejoin": "miter",
    "stroke-miterlimit": 4,
    "stroke-opacity": 1,
    "stroke-width": 1,
    "text-rendering": "auto",
    "alignment-baseline": 0,
    "baseline-shift": "baseline",
    "dominant-baseline": "auto",
    "glyph-orientation-horizontal": 0,
    "glyph-orientation-vertical": "auto",
    kerning: "auto",
    "text-anchor": "start",
    "writing-mode": "lr-tb"
};

function funk(func, obj, arg) {
    return function () { func.call(obj, arg); };
}

/**
 * Removes the leading and trailing spaces chars from the string.
 * NOTE: part of ES5, so use feature detection
 * http://stackoverflow.com/questions/2308134/trim-in-javascript-not-working-in-ie/#2308157
 * NOTE: the regular expression used in fallback is placed in global namespace for performance
 * (as it's far better having a "singleton" than bloating every string instance)
 */
if (typeof String.prototype.trim !== "function") {
    window._trimRegExp = new RegExp("^\\s+|\\s+$", "g");
    String.prototype.trim = function () {
        return this.replace(window._trimRegExp, "");
    };
}

/**
 * Set an ISO 8601 timestamp to a Date object.
 * NOTE: as ES5 doesn't define precisely what "parse" should do, we run a sample to test for feasibility
 * http://stackoverflow.com/questions/2479714/does-javascript-ecmascript3-support-iso8601-date-parsing/#2481375
 * NOTE: the regular expression used in fallback is placed in global namespace for performance
 * (as it's far better having a "singleton" than bloating every date instance)
 */
if (!isNaN(Date.parse("2012-04-22T19:53:32Z"))) {
    // parse did well, use the native implementation
    Date.prototype.setISO8601 = function (string) {
        this.setTime(Date.parse(string));
    };
} else {
    window._setISO8601RegExp = new RegExp(
		"([0-9]{4})(?:-([0-9]{2})(?:-([0-9]{2})" +
		"(?:T([0-9]{2}):([0-9]{2})(?::([0-9]{2})(?:\.([0-9]+))?)?" +
		"(?:Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?"
	);
    Date.prototype.setISO8601 = function (string) {
        var d = window._setISO8601RegExp.exec(string);

        // check that RegExp was applied successfully and that at least year is present
        if (d && d.length > 1) {
            var date = new Date(d[1], 0, 1);
            if (d[2]) { date.setMonth(d[2] - 1); }
            if (d[3]) { date.setDate(d[3]); }
            if (d[4]) { date.setHours(d[4]); }
            if (d[5]) { date.setMinutes(d[5]); }
            if (d[6]) { date.setSeconds(d[6]); }
            // NOTE: ISO 8601 "decimal fraction of a second" needs to be converted to milliseconds
            if (d[7]) { date.setMilliseconds(parseFloat("0." + d[7]) * 1000); }
            if (d[8]) {
                var offset = (parseInt(d[10]) * 60) + parseInt(d[11]);
                if (d[9] != '-') { offset = -offset; }
            } else
                var offset = 0;
            offset -= date.getTimezoneOffset();
            this.setTime(date.getTime() + (offset * 60 * 1000));
        } else
            this.setTime(NaN);
    };
}

try {
    // NOTE: ASV skips triggering the library here, as 'addEventListener' is not supported
    // (but that's not an issue as most popular versions, ASV3 and ASV6 beta, both support SMIL)
    window.addEventListener("load", initSMIL, false);
} catch (exc) { }
